
package io.renren.utils.util;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.dom4j.tree.DefaultAttribute;

/**
 *  System configuration manager
 * @author Clark Liu
 */
public class SystemConfigUtil { 
	// ----------------------------------------------------- Static Variables
	static Logger logger = Logger.getLogger(SystemConfigUtil.class.getName());
	
	public static final String SYSTEM_CONFIG_FILE = "config.properties";
	public static final String SYSTEM_CONFIG_PATH = "configpath";

	private static SystemConfigUtil _INSTANCE;

	// ----------------------------------------------------- Instance Variables

	private Document doc;

	private Pattern pattern = Pattern.compile("\\$\\{[\\w\\.]+\\}");

	private Map currentKeys = new HashMap();

	private String currentPathName = null;

	private SystemConfigUtil() {
	}
	
    static {
	logger = Logger.getLogger(SystemConfigUtil.class);
	logger.warn ("INITIALIZING THE CONFIGMANAGER");
	try {
	    loadFromClassPath();
	}
	catch (Exception ee) {
	    logger.warn ("UNABLE TO LOAD PROPERTIES - THE SYSTEM WILL NOT WORK ", ee);
	}

	logger.warn ("DONE INITIALIZING THE CONFIGMANAGER");

    }

	/**
	 * Create a new <code>ConfigManager</code> instance by giving a pathname string.
	 *  
	 * @param path
	 * @return
	 * @throws NullPointerException
	 *         if <code>path</code> argument is <code>null</code>
	 * @throws FileNotFoundException
	 *         if directory of <code>path</code> cannot read,
	 *         or cannot find any xml file in the directory.
	 */
	public static SystemConfigUtil getInstance() {
		if (_INSTANCE == null)
			_INSTANCE = new SystemConfigUtil();

		return _INSTANCE;
	}

    private static void loadFromClassPath () {
		try {
			ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
			InputStream propIn = classLoader.getResourceAsStream(SYSTEM_CONFIG_FILE);
			if (propIn != null) {
				Properties configProperties = new Properties();
				configProperties.load( propIn );
				String configFilePathArg = configProperties.getProperty(SYSTEM_CONFIG_PATH);
						
				logger.warn ("Loading config xml file using from : " + configFilePathArg);
		
				if (configFilePathArg!=null) {
					String[] configFilePaths = configFilePathArg.split(";");
					String configFilePath = null;
					for (int i = 0; i < configFilePaths.length; i++) {
						configFilePath = configFilePaths[i];
						if (configFilePath.length()>0) {
							if (configFilePath.startsWith("classpath:")) {
								configFilePath = configFilePath.substring("classpath:".length());
								SystemConfigUtil.getInstance().loadFromClassLoader(configFilePath);
							} else if (configFilePath.startsWith("/WEB-INF/")) {
								//
							} else  {
								SystemConfigUtil.getInstance().load(configFilePaths[i]);
							}
						}
					}
				}
		  
			} else {
				  throw new Exception("error loading properties: cannot find "+SYSTEM_CONFIG_FILE+"");
			}
		}
		catch (Exception e) {
		    logger.error("error loading properties ", e);
		}
    }
    
	public void load(String path) throws FileNotFoundException {
		if (path == null)
			throw new NullPointerException("Cannot load config from a null path.");
		File file = new File(path);
		if (file.exists())
			loadDocumentsInDir(file);
		else {
			logger.warn("File or directory [" + file.getAbsolutePath() + "] is not found!");
		}
	}

	/**
	 * Load all xml files in directory <code>dir</code>
	 * @param dir
	 * @throws FileNotFoundException
	 *         if directory of <code>path</code> cannot read,
	 *         or cannot find any xml file in the directory.
	 */
	private void loadDocumentsInDir(File dir) throws FileNotFoundException {
		File[] files;
		if (dir.isFile()) {
			files = new File[] { dir };
		} else {
			// List all xml files
			files = dir.listFiles(new FilenameFilter() {
				public boolean accept(File dir, String name) {
					return name.toLowerCase().endsWith(".xml");
				}
			});
		}
		if (files != null && files.length > 0) {
			SAXReader reader = new SAXReader();
			currentKeys.clear();
			currentPathName = dir.getAbsolutePath();
			for (int i = 0; i < files.length; i++) {
				logger.info("Load file:" + files[i]);
				readDocument(reader, files[i]);
			}
		} else {
			logger.warn("Cannot find any configuation file in the path [" + dir.getAbsolutePath() + "]!");
		}
	}


	/**
	 * Get classes from package
	 * 
	 * @param pack
	 * @return
	 */
	public void loadFromClassLoader(String packageDirName) {

		// 第一个class类的集合
		List xmlFiles = new ArrayList();
		String xmlFilePath = null;
		// 是否循环迭代
		// 获取包的名字 并进行替换
		//String packageDirName = packageName.replace('.', '/');
		// 定义一个枚举的集合 并进行循环来处理这个目录下的things
		Enumeration<URL> dirs;
		try {
			dirs = Thread.currentThread().getContextClassLoader().getResources(
					packageDirName);
			// 循环迭代下去
			while (dirs.hasMoreElements()) {
				// 获取下一个元素
				URL url = dirs.nextElement();
				// 得到协议的名称
				String protocol = url.getProtocol();
				// 如果是以文件的形式保存在服务器上
				if ("file".equals(protocol)) {
					// 获取包的物理路径
					String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
					// 以文件的方式扫描整个包下的文件 并添加到集合中
					loadDocumentsInDir(new File(filePath));
				} else if ("jar".equals(protocol)) {
					// 如果是jar包文件
					// 定义一个JarFile
					JarFile jar;
					try {
						// 获取jar
						jar = ((JarURLConnection) url.openConnection())
								.getJarFile();
						// 从此jar包 得到一个枚举类
						Enumeration<JarEntry> entries = jar.entries();
						// 同样的进行循环迭代
						while (entries.hasMoreElements()) {
							// 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件
							JarEntry entry = entries.nextElement();
							String name = entry.getName();
							
							// 如果是以/开头的
							if (name.charAt(0) == '/') {
								// 获取后面的字符串
								name = name.substring(1);
							}
							// 如果前半部分和定义的包名相同
							if (name.startsWith(packageDirName)) {
								if (name.toLowerCase().endsWith(".xml")) {
									//logger.info("Load xml file "+name+" from jar " + url);
									xmlFiles.add(name);
								}
							}
						}
					} catch (IOException e) {
						// log.error("在扫描用户定义视图时从jar包获取文件出错");
						e.printStackTrace();
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		for (int i = 0; i < xmlFiles.size(); i++) {
			xmlFilePath = (String)xmlFiles.get(i);
			readDocumentFromClassLoader(xmlFilePath);
		}
	}
	
	/**
	 * Load one <code>file</code>
	 * @param reader
	 * @param file
	 */
	private void readDocument(SAXReader reader, File file) {
		Document currDoc = null;
		try {
			currDoc = reader.read(file);
		} catch (DocumentException e) {
			logger.warn("file :" + file.getAbsolutePath() + " is a invalid config file!");
			/* ignore exception */
		}

		if (currDoc != null) {
			copyDocument(currDoc);
		}
	}
	
	/**
	 * Load one <code>file</code>
	 * @param reader
	 * @param file
	 */
	private void readDocumentFromClassLoader(String xmlFilePath) {
		InputStream byteStream = null;
		logger.info("Load xmlFilePath from class loader:" +xmlFilePath);
		try {
			ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
			if (classLoader == null) {
				classLoader = SystemConfigUtil.class.getClass().getClassLoader();
			}
			byteStream = classLoader.getResourceAsStream(xmlFilePath);
			
			SAXReader reader = new SAXReader();
			currentKeys.clear();
			currentPathName = xmlFilePath;			
			Document currDoc = null;
			
			try {
				currDoc = reader.read(byteStream);
			} catch (DocumentException e) {
				logger.warn("InputStream is a invalid config file!");
				/* ignore exception */
			}

			if (currDoc != null) {
				copyDocument(currDoc);
			}
			
			byteStream = null;
		} catch (Exception ex) {
			ex.printStackTrace();
		} finally {
			if (byteStream!=null) {
				try {
					byteStream.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
		
	}
	

	/**
	 * Copy all elements and attributes to <code>doc</code>
	 * @param srcDoc
	 */
	private void copyDocument(Document srcDoc) {
		if (doc == null) {
			doc = DocumentHelper.createDocument();
			doc.addElement("Property");
		}

		Element descRoot = doc.getRootElement();
		Element srcRoot = srcDoc.getRootElement();
		copyElement(descRoot, srcRoot);

		//        org.dom4j.io.OutputFormat format = org.dom4j.io.OutputFormat.createPrettyPrint();
		//        //format.setEncoding("UTF-8");
		//        try {
		//        	java.io.OutputStreamWriter streamWriter = new java.io.OutputStreamWriter(new java.io.FileOutputStream("D:\\WorkDir\\java\\gsd\\db\\last.xml"),"UTF-8");
		//        	org.dom4j.io.XMLWriter writer = new org.dom4j.io.XMLWriter(streamWriter);
		//        	
		//            writer.write(doc);
		//            writer.close();
		//        } catch (java.io.IOException e) {
		//        	e.printStackTrace();
		//            // ignore exception
		//        }
	}

	/**
	 * Copy all chindren elements and attributes of <code>srcElement</code> to <code>destElement</code> 
	 * @param destElement
	 * @param srcElement
	 */
	private void copyElement(Element destElement, Element srcElement) {
		Iterator attrs = srcElement.attributeIterator();
		Attribute attribute = null;
		String comeFromPathName = null;
		while (attrs.hasNext()) {
			attribute = (Attribute) attrs.next();
			comeFromPathName = (String) currentKeys.get(attribute.getPath());
			if (destElement.attribute(attribute.getName()) != null) {
				if (comeFromPathName == null || !currentPathName.equals(comeFromPathName)) {
					logger.warn("WARN: Find reduplicate attribute \"" + attribute.getPath() + "\", ignored it!");
					continue;
				}
			}
			//            System.out.println("copy attribute: " + attribute.getName());
			destElement.addAttribute(attribute.getName(), attribute.getValue());
			currentKeys.put(attribute.getPath(), currentPathName);
		}

		destElement.setText(srcElement.getTextTrim());
		Iterator eles = srcElement.elementIterator();
		Element fromElement;
		Element toElement = null;

		int count = 0;
		while (eles.hasNext()) {
			count++;
			fromElement = (Element) eles.next();
			//System.out.println("copy element:" + fromElement.getName());
			toElement = destElement.element(fromElement.getName());
			if (count == 1 && toElement != null) {
				comeFromPathName = (String) currentKeys.get(toElement.getPath());
				if (comeFromPathName == null || !currentPathName.equals(comeFromPathName)) {
					logger.warn("WARN: Find reduplicate element \"" + toElement.getPath() + "\", ignored it!");
					continue;
				}
			} else {
				toElement = destElement.addElement(fromElement.getName());
			}
			currentKeys.put(toElement.getPath(), currentPathName);
			copyElement(toElement, fromElement);
		}
	}

	/**
	 * Get property value
	 * @param key
	 * @return property value, null if not found. 
	 * @throws NullPointerException
	 *         if the <code>key</code> argument is <code>null</code>
	 */
	public String getProperty(String key) {
		if (key == null)
			throw new NullPointerException("Key cannot be a null.");
		if (doc == null)
			return null;

		//        System.out.println("Key:" + key);

		if (key.indexOf('.') > -1) {
			String attKey = key.substring(key.lastIndexOf('.') + 1, key.length());
			String eleKey = key.substring(0, key.lastIndexOf('.'));
			//            System.out.println("attKey:" + attKey);
			//            System.out.println("eleKey:" + eleKey);
			StringBuffer bKey = new StringBuffer();
			bKey.append("//Property/").append(eleKey.replace('.', '/'));
			//            System.out.println(indexUpgrade(bKey.toString()));
			Element ele = (Element) doc.selectSingleNode(indexUpgrade(bKey.toString()));
			if (ele == null)
				return null;
			return interpretValue(ele.attributeValue(attKey));
		} else {
			Element root = doc.getRootElement();
			return interpretValue(root.attributeValue(key));
		}
	}

	/**
	 * Get attribute map from key
	 * 
	 * @param key
	 * @return
	 */
	public Map getAttributeMap(String key) {
		Attribute[] attrs = getAttributes(key, true);
		Map result = new HashMap();
		for (int i = 0; i < attrs.length; i++) {
			result.put(attrs[i].getName(), attrs[i].getValue());
		}
		return result;
	}

	/**
	 * Get property value
	 * @param key
	 * @return property value, null if not found. 
	 * @throws NullPointerException
	 *         if the <code>key</code> argument is <code>null</code>
	 */
	public String getStringProperty(String key) {
		return getStringProperty(key, null);
	}

	/**
	 * Get property value
	 * @param key
	 * @return property value, null if not found. 
	 * @throws NullPointerException
	 *         if the <code>key</code> argument is <code>null</code>
	 */
	public String getStringProperty(String key, String defaultValue) {
		String value = getProperty(key);
		return value != null ? value : defaultValue;
	}

	/**
	 * Get int property
	 * 
	 * @param key
	 * @return
	 */
	public int getIntProperty(String key) {
		return getIntProperty(key, 0);
	}

	/**
	 * Get int property
	 * If not exists, return <code>defaultValue</code>
	 * 
	 * @param key
	 * @param defaultValue
	 * @return
	 */
	public int getIntProperty(String key, int defaultValue) {
		try {
			return Integer.parseInt(getProperty(key));
		} catch (Exception e) {
			return defaultValue;
		}
	}

	/**
	 * Get boolean property
	 * Return <code>false</code> if not found.
	 * 
	 * @param key
	 * @return
	 */
	public boolean getBooleanProperty(String key) {
		return getBooleanProperty(key, false);
	}

	/**
	 * Get boolean property
	 * Return <code>defaultValue</code> if not found.
	 * 
	 * @param key
	 * @param defaultValue
	 * @return
	 */
	public boolean getBooleanProperty(String key, boolean defaultValue) {
		String value = getProperty(key);
		if (value != null)
			return "true".equalsIgnoreCase(value);
		else
			return defaultValue;
	}

	/**
	 * Get long property
	 * 
	 * @param key
	 * @param defaultValue
	 * @return
	 */
	public long getLongProperty(String key, long defaultValue) {
		try {
			return Long.parseLong(getProperty(key));
		} catch (Exception e) {
			return defaultValue;
		}
	}
	
	/**
	 * Get double property
	 * Return <code>0</code> if not found.
	 * 
	 * @param key
	 * @return
	 */
	public double getDoubleProperty(String key) {
		return getDoubleProperty(key, 0);
	}
	
	/**
	 * Get double property
	 * 
	 * @param key
	 * @param defaultValue
	 * @return
	 */
	public double getDoubleProperty(String key, double defaultValue) {
		try {
			return Double.parseDouble(getProperty(key));
		} catch (Exception e) {
			return defaultValue;
		}
	}

	/**
	 * Get element count
	 * 
	 * @param key
	 * @return
	 */
	public int getPropertyCount(String key) {
		return getElements(key, false).length;
	}

	/**
	 * Get attribute count
	 * 
	 * @param key
	 * @return
	 */
	public int getAttributeCount(String key) {
		return getAttributes(key, false).length;
	}

	public boolean isPropertySet(String key) {
		if (key == null)
			throw new NullPointerException("Key cannot be a null.");

		return doc.selectSingleNode(indexUpgrade(key)) != null;
	}

	//    /**
	//     * Get elements
	//     * @param key
	//     * @return all element, null if not found 
	//     * @throws NullPointerException
	//     *         if the <code>key</code> argument is <code>null</code>
	//     */
	//    public Element[] getElements(String key) {
	//        return getElements(key, true);
	//    }

	/**
	 * Get elements
	 * If <code>interpret</code> is true, will interpret all attributes value.
	 * @param key
	 * @param interpret
	 * @return all element, null if not found 
	 * @throws NullPointerException
	 *         if the <code>key</code> argument is <code>null</code>
	 */
	private Element[] getElements(String key, boolean interpret) {
		if (key == null)
			throw new NullPointerException("Key cannot be a null.");
		if (doc == null)
			return null;

		Element[] eles = new Element[0];
		if (key.indexOf('.') > -1) {
			StringBuffer bKey = new StringBuffer();
			bKey.append("//Property/").append(key.replace('.', '/'));

			//          System.out.println(indexUpgrade(bKey.toString()));

			eles = (Element[]) doc.selectNodes(indexUpgrade(bKey.toString())).toArray(eles);
		} else {
			Element root = doc.getRootElement();
			eles = (Element[]) root.elements(key).toArray(eles);
		}
		//        System.out.println("eles:"+eles.length);
		if (interpret)
			return interpretElements(eles);
		return eles;
	}

	/**
	 * Get attributes
	 * If <code>interpret</code> is true, will interpret all attributes value.
	 * @param key
	 * @param interpret
	 * @return
	 */
	private Attribute[] getAttributes(String key, boolean interpret) {
		if (key == null)
			throw new NullPointerException("Key cannot be a null.");
		if (doc == null)
			return null;

		StringBuffer bKey = new StringBuffer();
		bKey.append("//Property/").append(key.replace('.', '/'));
		Element e = (Element) doc.selectSingleNode(bKey.toString());
		if (e == null || e.attributeCount() == 0)
			return new Attribute[0];
		Attribute[] attrs = (Attribute[]) e.attributes().toArray(new Attribute[0]);

		if (interpret)
			return interpretAttributes(attrs);
		return attrs;
	}

	/**
	 * Upgrade index
	 * 
	 * @param key
	 * @return
	 */
	private String indexUpgrade(String key) {
		Pattern p = Pattern.compile("(\\[)(\\d+)(\\])");
		Matcher m = p.matcher(key);
		StringBuffer sb = new StringBuffer(key.length());
		int lastEnd = 0;
		while (m.find()) {
			int index = Integer.parseInt(m.group(2)) + 1;
			sb.append(key.substring(lastEnd, m.start()));
			sb.append('[').append(index).append(']');
			lastEnd = m.end();
		}
		sb.append(key.substring(lastEnd, key.length()));

		return sb.toString();
	}

	/**
	 * Interpret a element
	 * @param ele
	 * @return
	 */
	private Element interpretElement(Element ele) {
		if (ele == null)
			return null;

		Element result = DocumentHelper.createElement(ele.getName());

		Iterator atts = ele.attributeIterator();
		Attribute attribute;
		while (atts.hasNext()) {
			attribute = (Attribute) atts.next();
			result.addAttribute(attribute.getName(), interpretValue(attribute.getValue()));
		}

		Iterator eles = ele.elementIterator();
		Element element;
		while (eles.hasNext()) {
			element = (Element) eles.next();
			result.add(interpretElement(element));
		}

		return result;
	}

	/**
	 * Interpret a element array
	 * @param eles
	 * @return
	 */
	private Element[] interpretElements(Element[] eles) {
		if (eles == null)
			return null;

		Element[] result = new Element[eles.length];
		for (int i = 0; i < eles.length; i++) {
			result[i] = interpretElement(eles[i]);
		}

		return result;
	}

	/**
	 * Interpret a attribute array
	 * 
	 * @param attrs
	 * @return
	 */
	private Attribute[] interpretAttributes(Attribute[] attrs) {
		if (attrs == null)
			return null;

		Attribute[] result = new Attribute[attrs.length];
		for (int i = 0; i < attrs.length; i++) {
			result[i] = new DefaultAttribute(attrs[i].getName(), interpretValue(attrs[i].getValue()));
		}

		return result;
	}

	/**
	 * Interpret a attribute value. 
	 * If find <code>${?}</code> expression, then replace with property value of inner key.
	 * @param value
	 * @return
	 */
	private String interpretValue(String value) {
		if (value == null)
			return null;
		String tmpValue = new String(value);
		Matcher matcher;
		int start = -1;
		int end = -1;
		String innerKey;
		while ((matcher = pattern.matcher(tmpValue)).find()) {
			//            System.out.println("tmpValue:" + tmpValue);
			try {
				start = matcher.start();
				end = matcher.end();
				innerKey = tmpValue.substring(start + 2, end - 1);
				//                System.out.println("innerKey:" + innerKey);
				tmpValue = replace(tmpValue, start, end, getProperty(innerKey));
			} catch (Exception e) { /* ignore exception */
			}
		}
		return tmpValue;
	}

	/**
	 * Replace from <code>start</code> to <code>end</code> of <code>old</code> string with <code>replace</code> string
	 * @param old
	 * @param start
	 * @param end
	 * @param replace
	 * @return
	 */
	private String replace(String old, int start, int end, String replace) {
		if (old == null)
			return null;
		if (start < 0 || end > old.length())
			throw new IndexOutOfBoundsException("start or end is wrong.");
		return new StringBuffer(old.length()).append(old.substring(0, start)).append(replace)
				.append(old.substring(end)).toString();
	}

	private String getNodeText(String key) {
		if (key == null)
			throw new NullPointerException("Key cannot be a null.");
		if (doc == null)
			return null;

		if (key.indexOf('.') > -1) {
			String eleKey = key;
			StringBuffer bKey = new StringBuffer();
			bKey.append("//Property/").append(eleKey.replace('.', '/'));
			Element ele = (Element) doc.selectSingleNode(indexUpgrade(bKey.toString()));
			if (ele == null)
				return null;
			return interpretValue(ele.getTextTrim());
		} else {
			Element root = doc.getRootElement();
			return interpretValue(root.attributeValue(key));
		}
	}

}
